*! version 1.5 31DEC2019  Benjamin Daniels bbdaniels@gmail.com

// Betterbar - Stata module to produce bar graphs with standard error bars and cross-group comparisons.

cap prog drop betterbar
prog def betterbar

	// Version compatibility
	if `c(version)' >= 15 local la "la(center)"
	version 13.1

syntax anything 				    /// Variable list
	[if] [in] [fw iw aw pw], 	///
  [by(varname)]				      /// Separate variables across higher groups
	[Over(varname)]				    /// Determines groups for comparison at the lowest level
	///
  [n]							          /// Adds sample sizes in legend
  [ci]						          /// Plots standard error bars
  [vce(passthru)]           /// Allow any VCE options in [mean]
	[BARlab]					        /// Labels bars with means
  [BARColor(string asis)]   /// Specify list of bar colors
  [pct]                     /// Bar labels as percentages
	[format(string asis)]		  /// Formats for bar means
  [Vertical]					      /// Horizontal bar is the default
	[*]							          /// Allows any normal options for twoway graphs


// Prep

qui {
preserve
marksample touse , novarlist
	keep if `touse'

// Setup

	// Clean for weights
	local anything = subinstr("`anything'","[]","",.)

  // Set up bar colors
  if "`barcolor'" != "" {
    local colorCounter = 0
    foreach color in `barcolor' {
      local ++colorCounter
      local barColor`colorCounter' = ///
        "fc(`: word `=`: list sizeof barcolor' - `colorCounter' + 1' of `barcolor'')"
    }
  }

	// If no by or over - fill in
		if "`by'" == "" {
			tempname fakebyvar
			gen `fakebyvar' = 1
			local by = "\`fakebyvar'"
			local byoff = 1
		}
		else local byoff = 0

		if "`over'" == "" {
			tempname fakeovervar
			gen `fakeovervar' = 1
			local over = "\`fakeovervar'"
			label def fakeover 1 "Total"
				label val `fakeovervar' fakeover
		}

	// Save this dataset for loops
	tempfile allData
		save `allData' , replace

	// Initialize results dataset
	clear
		tempfile results
		save `results' , emptyok

	// Horizonal-vertical settings
	if "`vertical'" == "" local horizontal "horizontal"
	if "`horizontal'" != "" {
		local axis y
	}
	else {
		local axis x
	}

// Get means and bounds

	// Loop over by-groups
	use `allData' , clear
  unab theVarlist: `anything'
	levelsof `by' , local(bylevels)
	foreach bylevel in `bylevels' {
		// Mean respecting over-groups
		use `allData' , clear
    unab anything : `anything'
		foreach var of varlist `anything' {
			count if `by' == `bylevel' & `var' < .
			if `r(N)' == 0 replace `var' = 0 if `by' == `bylevel'
		}
		keep if `by' == `bylevel'

    tempname a
    cap mat drop `a'
    foreach var of varlist `anything' {
  		mean `var' [`weight'`exp'] ///
        , over(`over' , nolabel) `vce'

      mat theseStats = r(table)
      mat `a' = nullmat(`a') ///
        , theseStats
    }

		clear
		svmat `a' , n(eqcol)

    foreach var in `theVarlist' {
      foreach lab in `e(over_labels)' {
        rename `var'`lab' `var'_`lab'
      }
    }
		rename * stat_*

		gen `by' = `bylevel'

		local x = 0
		foreach var of varlist stat_* {
			replace `var' = `x' in 9
			local ++x
		}

		append using `results'
		save `results' , replace
	}

// Set up graphing points

	// Find means and bounds
    tempvar type
		gen `type' = mod(_n,9)
		reshape long stat_ , i(`by' `type') j(n) string

	// Sort order
		tempvar temp
		gen `temp' = stat_ if `type' == 0
		bys n: egen order = min(`temp')
		drop `temp'

		gen so = 0
		local item_n = -1
		foreach item in `anything' {
			replace so = `item_n' if substr(n,1,strrpos(n,"_")-1) == "`item'"
			local --item_n
		}

    tempvar overvar
    gen `overvar' = real(substr(n,-1,.))

    if "`vertical'" != "" {
      gsort + `by' + so + `overvar' + n + order + `type'
    }
    else {
      gsort - `by' + so - `overvar' + n + order + `type'
    }

		keep if `type' == 1 | `type' == 5 | `type' == 6
		gen place = _n - mod(_n,3)
			replace place = place - 3 if mod(_n,3) == 0
			drop order

			reshape wide stat_ , i(n `by') j(`type')
			sort place

	// Gaps
		local x = 0
		count
		forvalues i = 1/`r(N)' {
			if `by'[`i'] != `by'[`=`i'-1'] local x = `x'+3
			replace place = place + `x' - 1 in `i'
		}

	// Save statistics dataset
	save `results' , replace

// Prep the graph

	// Graph commands with info from base dataset
	use `allData' , clear
		local x = 0
		levelsof `over' , local(olevels)
		foreach level in `olevels' {
			local ++x
			local theLabel : label (`over') `level'
			count if `over' == `level'
			if "`n'" != "" local theN " (N=`r(N)')"
			local theBars `"`theBars' (bar stat_1 place if n == "`level'" , `barColor`x'' barw(2) fi(100) lw(thin) `la' lc(white) `horizontal' ) "'
			local theLegend `"`theLegend' `x' "`theLabel'`theN'""'
		}
		// Get variable labels
		foreach var in `anything' {
			local `var' : var label `var'
		}
		// By-labels
		foreach level in `bylevels' {
			local `level' : label (`by') `level'
		}

	// Use statistics dataset for final graph
	use `results' , clear

		// Set up CI plot
		if "`ci'" == "ci" local ++x
		if "`ci'" == "ci" local ciplot `"(rspike stat_5 stat_6 place , lc(black) `horizontal' legend(label(`x' "95% CI")))"'
		local ++x

		// Set up bar labels
		gen lab = strofreal(stat_1,"%9.2f")
    if "`pct'" == "pct" replace lab = subinstr(lab,".0",".",.)
    if "`pct'" == "pct" replace lab = subinstr(lab,"0.","",.) + "%"
    if "`pct'" == "pct" replace lab = subinstr(lab,"1.","1",.)
		if "`format'" != "" replace lab = strofreal(stat_1,"`format'")
		if "`barlab'" != "" & "`ci'" == "ci" & "`vertical'" == "" local blabplot "(scatter place stat_6  , m(none) mlab(lab) mlabpos(3) mlabc(black) )"
		if "`barlab'" != "" & "`ci'" == ""   & "`vertical'" == "" local blabplot "(scatter place stat_1  , m(none) mlab(lab) mlabpos(3) mlabc(black) )"
		if "`barlab'" != "" & "`ci'" == "ci" & "`vertical'" != "" local blabplot "(scatter stat_6 place , m(none) mlab(lab) mlabpos(12) mlabc(black) )"
		if "`barlab'" != "" & "`ci'" == ""   & "`vertical'" != "" local blabplot "(scatter stat_1 place , m(none) mlab(lab) mlabpos(12) mlabc(black) )"
    if "`barlab'" != "" & "`ci'" == "ci" replace stat_6 = stat_1 if stat_6 == .

		// Set up variable names

		gen var = ""
		foreach var in `anything' {
			replace var = "``var''" if substr(n,1,strrpos(n,"_")-1) == "`var'"
			replace n = substr(n,strrpos(n,"_")+1,.) if substr(n,1,strrpos(n,"_")-1) == "`var'"
		}

		// Gaps
		count
		forvalues i = 2/`r(N)' {
			if var[`i'] != var[`=`i'-1'] replace place = place + 3 if _n >= `i'
		}

		// Set up by-levels
		foreach level in `bylevels' {
			foreach var in `anything' {
				su place if `by' == `level' & var == "``var''"
				if `byoff' != 1 & "`r(mean)'" != "" local varlabs `"`varlabs' `r(mean)' "``level''"  "'
				else if "`r(mean)'" != "" local varlabs `"`varlabs' `r(mean)' "``var''"  "'
			}
		}

// Make the graph

	gen zero = 0

	tw ///
		`theBars' 	///
		`ciplot' 	///
		`blabplot' 	///
		(scatter zero zero , m (none) ) ///
		, xtitle(" ") ytitle(" ") legend(order(`theLegend')) ///
			`axis'lab(`varlabs' , angle(0) nogrid notick) ylab(,angle(0)) ///
			`options'

// end qui
}
end
// Have a lovely day!